/*
 * Encrypt/decrypt plugin using DES-CBC.
 *
 * Copyright (c) 2010-2012 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/crypto.h>
#include <connman/log.h>
#include <connman/option.h>
#include <connman/plugin.h>
#include <errno.h>
#include <glib.h>
#include <string.h>

#include <rpc/des_crypt.h>

#define BLOCK_SIZE 8

static gboolean have_keymatter = FALSE;
static char des_key[BLOCK_SIZE];
static char des_iv[BLOCK_SIZE];

/*
 * This is prepended to the base64 encoded value so that we can alter the
 * encryption or encoding schemes over time.
 */
static char version_2_prefix[] = "02:";
static int version_prefix_length = 3;

/*
 * This is appended to the plaintext before encryption, and checked during
 * decryption to detect decryption failures.  This is at the end of the string
 * rather than the beginning to simplify its removal during decryption.
 * This was introduced in version 2.
 */
static char sentinel_suffix[] = "[ok]";
static int sentinel_length = 4;

static char *descbc_encrypt(const char *key, const char *value);
static char *descbc_decrypt(const char *key, const char *value);

static struct connman_crypto crypto_descbc = {
	.name                   = "des-cbc",
	.priority               = CONNMAN_CRYPTO_PRIORITY_HIGH,
	.encrypt_keyvalue       = descbc_encrypt,
	.decrypt_keyvalue       = descbc_decrypt,
};

static char *descbc_encrypt(const char *key, const char *value)
{
	/*
	 * NB: never encrypt; we'll fallback to something like rot47
	 * which doesn't depend on the owner key which may change due
	 * to rotation.
	 */
	return NULL;
}

static char *descbc_decrypt(const char *key, const char *value)
{
	gsize ciphertext_size;
	int rv;
	int version = 1;
	char *buf = NULL;
	char local_iv[BLOCK_SIZE];

	if (have_keymatter == FALSE)
		return NULL;

	if (g_str_has_prefix(value, version_2_prefix)) {
		version = 2;
		value += version_prefix_length;
	}

	/*
	 * Buf starts out a the ciphertext, but becomes the plaintext since the
	 * crypt operation happens in place.  This function returns something
	 * that must g_free()d, even if it is handed a 0 length value.
	 */
	buf = (char *)g_base64_decode(value, &ciphertext_size);

	/* If ciphertext is not a whole number of blocks, something is wrong. */
	if (ciphertext_size == 0 || (ciphertext_size % BLOCK_SIZE != 0))
		goto bad;

	/*
	 * Make a copy of the IV, since this is also modified in place during
	 * the crypt.
	 */
	memcpy(local_iv, des_iv, BLOCK_SIZE);

	rv = cbc_crypt(des_key, buf, ciphertext_size, DES_DECRYPT, local_iv);
	if (DES_FAILED(rv)) {
		connman_error("des-cbc: cbc_crypt failed.");
		goto bad;
	}

	if (version == 2) {
		if (g_str_has_suffix(buf, sentinel_suffix) == FALSE) {
			connman_error("des-cbc: Decrypt failed: bad key?");
			goto bad;
		}

		buf[strlen(buf) - sentinel_length] = '\0';
	}

	return buf;

bad:
	g_free(buf);
	return NULL;
}

static int load_keymatter(const char* keymatter_file)
{
	gboolean result;
	gchar *keymatter;
	gsize keymatter_size;

	connman_info("des-cbc: Reading keymatter file: %s", keymatter_file);

	result = g_file_get_contents(keymatter_file, &keymatter,
	    &keymatter_size, NULL);
	if (result == FALSE) {
		connman_error("des-cbc: Error reading %s: %s",
		    keymatter_file, strerror(errno));
		return -errno;
	}

	if (keymatter_size == 0) {
		connman_error("des-cbc: Empty key file: %s", keymatter_file);
		return -ENOENT;
	}

	if (keymatter_size < 2 * BLOCK_SIZE) {
		connman_error("des-cbc: Keymatter file is too small, expected "
		    "at least %i bytes, found %zu",
		    2 * BLOCK_SIZE, keymatter_size);
		return -ENOENT;
	}

	memcpy(des_key, keymatter + keymatter_size - BLOCK_SIZE, BLOCK_SIZE);
	memcpy(des_iv, keymatter + keymatter_size - 2 * BLOCK_SIZE, BLOCK_SIZE);

	g_free(keymatter);

	return 0;
}

static int setup_keymatter(void)
{
	const char *keymatter_file;

	keymatter_file = connman_option_get_string("keymatter");
	if (keymatter_file == NULL) {
		connman_warn("des-cbc: No keymatter file specified");
		return FALSE;
	}
	return (load_keymatter(keymatter_file) == 0);
}

static int descbc_init(void)
{
	have_keymatter = setup_keymatter();

	if (connman_crypto_register(&crypto_descbc) < 0) {
		connman_error("des-cbc: Failed to register plugin");
		return -1;
	}
	return 0;
}

static void descbc_finish(void)
{
	connman_crypto_unregister(&crypto_descbc);
}

CONNMAN_PLUGIN_DEFINE(crypto_descbc, "DES-CBC Plugin", VERSION,
    CONNMAN_PLUGIN_PRIORITY_DEFAULT, descbc_init,
    descbc_finish)
